#include <iostream>
#include <cstring>
#include <fstream>
#include <vector>
#include <cmath>
#include <limits>
#include <iomanip>

#include "XMLParser.h"
#include "Arguments.h"
#include "Algorithm.h"

// ----------------------------------------------------------------------------

namespace gpxtools
{
  class GPXJson : public XMLParserHandler
  {
  private:
    const std::string LATLON = "latlon";
    const std::string LONLAT = "lonlat";

  public:
    // -- Constructor -----------------------------------------------------------
    GPXJson() :
      _arguments("gpxjson [OPTION].. [FILE]\nDisplay the GeoJson data from a GPX-file on standard output.\n", "gpxjson v0.1", "Display the waypoints, routes and/or tracks in GeoJson format from a GPX-file on standard output."),
      _waypoints  (_arguments, true,  'w', "waypoints",                     "convert the waypoints"),
      _tracks     (_arguments, true,  't', "tracks",                        "convert the tracks"),
      _routes     (_arguments, true,  'r', "routes",                        "convert the routes"),
      _format     (_arguments, true,  'f', "format",      "geojson|plain",  "set the output format (def:geojson)", "geojson"),
      _mode       (_arguments, true,  'm', "mode",        "compact|normal", "set the output mode (def:normal)", "normal"),
      _coordinates(_arguments, true,  'c', "coordinates", "lonlat|latlon",  "set the type of coordinates for plain format (def:lonlat)", LONLAT),
      _number     (_arguments, true,  'p', "points",      "NUMBER",         "set the number of points per line in normal mode (def:4)", "4"),
      _simplify   (_arguments, true,  's', "simplify",    "METRES",         "simplify the route by track distance", ""),
      _xmlParser  (this)
    {
    }

    // -- Deconstructor ---------------------------------------------------------
    virtual ~GPXJson()
    {
    }

    // -- Properties ------------------------------------------------------------

    // -- Parse arguments -------------------------------------------------------
    bool parseArguments(int argc, char *argv[])
    {
      std::vector<std::string> filenames;

      if (!_arguments.parse(argc,argv, filenames))
      {
        return false;
      }
      else if (!checkArguments(filenames))
      {
        return false;
      }
      else if (filenames.empty())
      {
        return _xmlParser.parse(std::cin);
      }
      else
      {
        return parseFile(filenames.front());
      }
    }

    // -- Check arguments -------------------------------------------------------
    bool checkArguments(const std::vector<std::string> &filenames)
    {
      if (filenames.size() > 1)
      {
        std::cerr << "Too many input files."  << std::endl;
        return false;
      }

      if ((_format.value() != "geojson") && (_format.value() != "plain"))
      {
        std::cerr << "Invalid value for --" << _format.longOption() << ": " << _format.value() << std::endl;
        return false;
      }

      if ((_mode.value() != "compact") && (_mode.value() != "normal"))
      {
        std::cerr << "Invalid value for --" << _mode.longOption() << ": " << _mode.value() << std::endl;
        return false;
      }

      if ((_coordinates.value() != LATLON) && (_coordinates.value() != LONLAT))
      {
        std::cerr << "Invalid value for --" << _mode.longOption() << ": " << _mode.value() << std::endl;
        return false;
      }

      try
      {
        int number = std::stoi(_number.value());

        if (number <= 0)
        {
          std::cerr << "Invalid value for --" << _number.longOption() << ": " << _number.value() << std::endl;
          return false;
        }
      }
      catch (...)
      {
        std::cerr << "Invalid number for --" << _number.longOption() << ": " << _number.value() << std::endl;
        return false;
      }

      if (_simplify.active())
      {
        try
        {
          double distance = std::stod(_simplify.value());

          if (distance <= 0.0)
          {
            std::cerr << "Invalid value for --" << _number.longOption() << ": " << _simplify.value() << std::endl;
            return false;
          }
        }
        catch(...)
        {
          std::cerr << "Invalid value for --" << _number.longOption() << ": " << _simplify.value() << std::endl;
          return false;
        }
      }

      return true;
    }

    // -- Parse a file ----------------------------------------------------------
    bool parseFile(const std::string &filename)
    {
      bool ok =false;

      std::ifstream file(filename);

      if (file.is_open())
      {
        ok = _xmlParser.parse(file);

        file.close();
      }
      else
      {
        std::cerr << "Unable to open: " << filename << std::endl;
      }

      return ok;
    }

    void outputJson(std::ostream &output)
    {
      std::size_t points = _points.size();
      std::size_t lines  = _lines.size();

      if (_format.value() == "geojson")
      {
        if (points == 1) outputSingleGeoJsonPoint(output);
        if (points >  1) outputMultipleGeoJsonPoints(output);

        if (lines == 1) outputSingleGeoJsonLine(output);
        if (lines >  1) outputMultipleGeoJsonLines(output);
      }
      else if (_format.value() == "plain")
      {
        if (points == 1) outputSinglePlainPoint(output);
        if (points >  1) outputMultiplePlainPoints(output);

        if (lines == 1) outputSinglePlainLine(output);
        if (lines >  1) outputMultiplePlainLines(output);
      }
    }

    void simplify()
    {
      if (!_simplify.active()) return;

      double setting = std::stod(_simplify.value());

      for (auto line = _lines.begin(); line != _lines.end(); ++line)
      {
        while (true)
        {
          auto p1 = line->end();
          auto p2 = line->end();
          auto p3 = line->begin();
          auto ps = line->end();
          double low = 9999.99;

          while (p3 != line->end())
          {
            if ((p1 != line->end()) && (p2 != line->end()))
            {
              double d = (gpx::calcDistance(p1->_lat, p1->_lon, p2->_lat, p2->_lon) +
                          gpx::calcDistance(p2->_lat, p2->_lon, p3->_lat, p3->_lon)) -
                          gpx::calcDistance(p1->_lat, p1->_lon, p3->_lat, p3->_lon);

              if (d < low)
              {
                low = d;
                ps  = p2;
              }
            }

            p1 = p2;
            p2 = p3;
            ++p3;
          }

          if ((low > setting) || (ps == line->end())) break;

          line->erase(ps);
        }
      }
    }

  private:
    void outputSingleGeoJsonPoint(std::ostream &output)
    {
      output << std::fixed << std::setprecision(6);
      output << '{'; doEndl(output);
      doIndent();
      output << _indent << "\"type\":\"Point\","; doEndl(output);
      output << _indent << "\"coordinates\":[" << _points.front()._lon << ',' << _points.front()._lat << "]"; doEndl(output);
      doOutdent();
      output << '}'; doEndl(output);
    }

    void outputMultipleGeoJsonPoints(std::ostream &output)
    {
      output << std::fixed << std::setprecision(6);
      output << '{'; doEndl(output);
      doIndent();
      output << _indent << "\"type\":\"MultiPoint\","; doEndl(output);
      output << _indent << "\"coordinates\":["; doEndl(output);
      doIndent();

      int number = std::stoi(_number.value());

      auto iter = _points.begin();
      while (iter != _points.end())
      {
        output << _indent;

        int count = 0;
        while (iter != _points.end() && count < number)
        {
          auto next = iter + 1;

          output << '[' << iter->_lon << ',' << iter->_lat << ']';

          if (next != _points.end()) output << ',';

          ++iter; count++;
        }
        doEndl(output);
      }

      doOutdent();
      output << _indent << "]"; doEndl(output);
      doOutdent();
      output << '}'; doEndl(output);
    }

    void outputSingleGeoJsonLine(std::ostream &output)
    {
      output << std::fixed << std::setprecision(6);
      output << '{'; doEndl(output);
      doIndent();
      output << _indent << "\"type\":\"LineString\","; doEndl(output);
      output << _indent << "\"coordinates\":["; doEndl(output);
      doIndent();

      int number = std::stoi(_number.value());

      auto iter = _lines.front().begin();
      auto end  = _lines.front().end();

      while (iter != end)
      {
        output << _indent;

        int count = 0;
        while (iter != end && count < number)
        {
          auto next = iter + 1;

          output << '[' << iter->_lon << ',' << iter->_lat << ']';

          if (next != end) output << ',';

          ++iter; count++;
        }
        doEndl(output);
      }

      doOutdent();
      output << _indent << "]"; doEndl(output);
      doOutdent();
      output << '}'; doEndl(output);
    }

    void outputMultipleGeoJsonLines(std::ostream &output)
    {
      output << std::fixed << std::setprecision(6);
      output << '{'; doEndl(output);
      doIndent();
      output << _indent << "\"type\":\"MultiLineString\","; doEndl(output);
      output << _indent << "\"coordinates\":["; doEndl(output);
      doIndent();

      int number = std::stoi(_number.value());

      output << _indent;
      for (auto iter2 = _lines.begin(); iter2 != _lines.end(); ++iter2)
      {
        auto next2 = iter2 + 1;

        output << "["; doEndl(output);
        doIndent();

        auto iter = iter2->begin();
        while (iter != iter2->end())
        {
          output << _indent;

          int count = 0;
          while (iter != iter2->end() && count < number)
          {
            auto next = iter + 1;

            output << '[' << iter->_lon << ',' << iter->_lat << ']';

            if (next != iter2->end()) output << ',';

            ++iter; count++;
          }
          doEndl(output);
        }
        doOutdent();

        output << _indent << "]";
        if (next2 != _lines.end()) output << ",";
      }
      doEndl(output);
      doOutdent();

      output << _indent << "]"; doEndl(output);
      doOutdent();

      output << "}"; doEndl(output);
    }

    void outputSinglePlainPoint(std::ostream &output)
    {
      output << std::fixed << std::setprecision(6);

      if (_coordinates.value() == LONLAT)
      {
        output << '[' << _points.front()._lon << ',' << _points.front()._lat << ']';
      }
      else
      {
        output << '[' << _points.front()._lat << ',' << _points.front()._lon << ']';
      }
      doEndl(output);
    }

    void outputMultiplePlainPoints(std::ostream &output)
    {
      const int columns = std::stoi(_number.value());
      const bool lonlat = (_coordinates.value() == LONLAT);

      output << std::fixed << std::setprecision(6);

      output << '['; doEndl(output);
      doIndent();

      auto point = _points.begin();
      while (point != _points.end())
      {
        output << _indent;

        int column = 0;
        while ((point != _points.end()) && (column < columns))
        {
          if (lonlat)
          {
            output << '[' << point->_lon << ',' << point->_lat << ']';
          }
          else
          {
            output << '[' << point->_lat << ',' << point->_lon << ']';
          }

          if ((point + 1) != _points.end()) output << ',';

          ++point; column++;
        }
        doEndl(output);
      }
      doOutdent();

      output << ']'; doEndl(output);
    }

    void outputSinglePlainLine(std::ostream &output)
    {
      const int columns = std::stoi(_number.value());
      const bool lonlat = (_coordinates.value() == LONLAT);

      output << std::fixed << std::setprecision(6);

      output << '['; doEndl(output);
      doIndent();

      auto point = _lines.front().begin();
      auto end = _lines.front().end();
      while (point != end)
      {
        output << _indent;

        int column = 0;
        while ((point != end) && (column < columns))
        {
          if (lonlat)
          {
            output << '[' << point->_lon << ',' << point->_lat << ']';
          }
          else
          {
            output << '[' << point->_lat << ',' << point->_lon << ']';
          }

          if ((point + 1) != end) output << ',';

          ++point; column++;
        }
        doEndl(output);
      }
      doOutdent();

      output << ']'; doEndl(output);
    }

    void outputMultiplePlainLines(std::ostream &output)
    {
      const int columns = std::stoi(_number.value());
      const bool lonlat = (_coordinates.value() == LONLAT);

      output << std::fixed << std::setprecision(6);

      output << '['; doEndl(output);
      doIndent();

      for (auto line = _lines.begin(); line != _lines.end(); ++line)
      {
        output << _indent << '['; doEndl(output);
        doIndent();

        auto point = line->begin();
        auto end = line->end();
        while (point != end)
        {
          output << _indent;

          int column = 0;
          while ((point != end) && (column < columns))
          {
            if (lonlat)
            {
              output << '[' << point->_lon << ',' << point->_lat << ']';
            }
            else
            {
              output << '[' << point->_lat << ',' << point->_lon << ']';
            }

            if ((point + 1) != end) output << ',';

            ++point; column++;
          }
          doEndl(output);
        }
        doOutdent();

        output << _indent << ']';

        if ((line + 1) != _lines.end()) output << ',';

        doEndl(output);
      }

      doOutdent();

      output << ']'; doEndl(output);
    }

    void doEndl(std::ostream &output)
    {
      if (_mode.value() == "normal") output << std::endl;
    }

    void doIndent()
    {
      if (_mode.value() == "normal") _indent += "  ";
    }

    void doOutdent()
    {
      if (_mode.value() == "normal") _indent.erase(_indent.size()-2);
    }

    static double getDoubleAttribute(const Attributes &atts, const std::string &key)
    {
      auto iter = atts.find(key);

      try
      {
        return iter != atts.end() ? std::stod(iter->second) : std::numeric_limits<double>::min();
      }
      catch(...)
      {
        return std::numeric_limits<double>::min();
      }
    }

  public:
    // -- Callbacks -------------------------------------------------------------
    virtual void startElement(const std::string &path, const std::string &, const Attributes &attributes)
    {
      if ((_tracks.active() && (path == "/gpx/trk/trkseg")) ||
          (_routes.active() && (path == "/gpx/rte")))
      {
        _line.clear();
      }
      else if ((_tracks.active() && (path == "/gpx/trk/trkseg/trkpt")) ||
               (_routes.active() && (path == "/gpx/rte/rtept")))
      {
        double lat = getDoubleAttribute(attributes, "lat");
        double lon = getDoubleAttribute(attributes, "lon");

        _line.push_back(Point(lat, lon));
      }
      else if (_waypoints.active() && (path == "/gpx/wpt"))
      {
        double lat = getDoubleAttribute(attributes, "lat");
        double lon = getDoubleAttribute(attributes, "lon");

        _points.push_back(Point(lat, lon));
      }
    }

    virtual void text(const std::string &, const std::string &)
    {

    }

    virtual void endElement(const std::string &path, const std::string &)
    {
      if ((_tracks.active() && (path == "/gpx/trk/trkseg")) ||
          (_routes.active() && (path == "/gpx/rte")))
      {
        _lines.push_back(_line);
      }
    }

  private:
    arg::Arguments           _arguments;

    arg::Argument            _waypoints;
    arg::Argument            _tracks;
    arg::Argument            _routes;
    arg::Argument            _format;
    arg::Argument            _mode;
    arg::Argument            _coordinates;
    arg::Argument            _number;
    arg::Argument            _simplify;

    XMLParser                _xmlParser;

    // Types
    struct Point
    {
      Point(double lat, double lon)
      {
        _lat        = lat;
        _lon        = lon;
      }

      double        _lat;
      double        _lon;
    };

    typedef std::vector<Point>  Line;

    // Members
    Line                _line;

    std::vector<Line>   _lines;
    std::vector<Point>  _points;

    std::string         _indent;
  };
}

// -- Main program ------------------------------------------------------------

int main(int argc, char *argv[])
{
  gpxtools::GPXJson gpxJson;

  if (gpxJson.parseArguments(argc, argv))
  {
    gpxJson.simplify();

    gpxJson.outputJson(std::cout);

    return 0;
  }

  return 1;
}
